Go 的 HTTP 标准库-HTTP2.0 的使用
学习项目源码时,看到了使用关于 http2 的包,使用的是 h2c,所以这里记录一下这个包的使用
http2 协议的安全问题,实际上与http1协议一样,同样可选择http/https两种,一种是明文传输,另一种是加密传递。只不过在 google 设计 http2 协议的时候,偏向了加密传输,以至于 golang 1.6 中默认支持的 http2 协议包,现在却没有办法实现非加密 http2 通信了。
在实现中,http2 分为两个类型,普通的加密称称为 http2/h2,非加密的称为 http2/h2c。 在使用中,http2/h2 一般都有官方标准的实现,而 http2/h2c 则支持力度次之。
而由于加密 http2/h2 由于在TLS握手阶段,可能消耗约~10ms 时间,在有些场合是非常大的开销,所以还可能要用到 http2/h2c 实现方式。
比如在 grpc 中,服务器之间通信要求更高,就自带了一个 http2/h2c 的实现。
http2/h2 类型server实现
默认的 ListenAndServe 实际只支持 http1 协议,因为没有提供 TLS 支持,不会执行 golang 实现的 http2 协议逻辑。
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r)
}),
}
srv.ListenAndServe()
}
如果要支持 http2 需要改成这样
srv.ListenAndServeTLS("local.cert", "local.key")
补充:这个证书和秘钥生成
# 生成私钥:
openssl genrsa -out key.pem 2048
# 生成证书:
openssl req -new -x509 -key key.pem -out cert.pem -days 3650
http2/h2c 类型server
标准的 golang 代码支持 HTTP2,但不直接支持 H2C。对 H2C 的支持只存在于 google 的 "golang.org/x/net/http2/h2c" 包中
如下创建一个 h2c 服务端,它支持 golang 本身支持的标准 HTTP/2和 HTTP/1.1
import (
"log"
"net"
"net/http"
"golang.org/x/net/http2/h2c"
"golang.org/x/net/http2"
)
func main() {
lsrv, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
srv := http.Server{
Handler: h2c.NewHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
log.Println(r)
rw.Write([]byte("hello world"))
}), &http2.Server{}),
}
if err := http2.ConfigureServer(&srv, &http2.Server{}); err != nil {
panic(err)
}
if err := srv.Serve(lsrv); err != nil {
panic(err)
}
}
这里使用 curl 工具发送 http2 请求
$ curl -v --http2 http://localhost:8080
检查控制台打印:
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
>
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 11
< date: Tue, 25 Jan 2022 03:29:17 GMT
<
* Connection #0 to host localhost left intact
hello world%
可以发现是通过 HTTP/1.1连接,然后升级到 HTTP/2(H2C)
注意看这里的 Connection: Upgrade
首部,浏览器不是通过这种方式升级使用 HTTP2 的,它是通过 HTTPS 的拓展字段进行协商升级的,具体看 HTTP2 那篇笔记
如果不需要支持 HTTP/1.1 可以这样写
func main() {
server := http2.Server{}
l, err := net.Listen("tcp", "0.0.0.0:8080")
log.Println(err, "while listening")
fmt.Printf("Listening [0.0.0.0:8080]...\n")
for {
conn, err := l.Accept()
log.Println(err, "during accept")
go func() {
server.ServeConn(conn, &http2.ServeConnOpts{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %v, http: %v", r.URL.Path, r.TLS == nil)
}),
})
}()
}
}
与 net/http 包使用方法不同,需要自己 listen,accept 并创建 goroutine 处理新连接请求。
这么实现的 server,为什么实现了 http2/h2c 协议呢? 因为我们跳过了 TLS 的握手协议创建加密连接一步,而是直接给 http2 请求处理函数一个明文的连接。
编写客户端连接
Golang 的标准库默认是不支持 h2c 的,所以标准包里面的客户端也不支持 h2c,所以必须重写 AllowHTTP
下面这个对接上面只支持 HTTP2 的服务端
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"golang.org/x/net/http2"
)
func main() {
client := http.Client{
Transport: &http2.Transport{
// So http2.Transport doesn't complain the URL scheme isn't 'https'
AllowHTTP: true,
// Pretend we are dialing a TLS endpoint.
// Note, we ignore the passed tls.Config
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
},
}
resp, _ := client.Get("http://localhost:8080")
fmt.Printf("Client Proto: %d\n", resp.ProtoMajor)
}
输出打印:
Client Proto: 2
这里的两个参数:
- AllowHTTP: 这个字段指定允许非TLS加密传输
- DialTLS: 需要覆盖默认的创建连接函数。
References
HTTP/2 Cleartext (H2C) Client Example in Go golang/Go中HTTP2.0协议的应用